package org.haredot.filter;

import eu.bitwalker.useragentutils.UserAgent;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.lang.Assert;
import org.haredot.config.JwtToken;
import org.haredot.constant.RabbitConst;
import org.haredot.context.LoginLongContextHolder;
import org.haredot.entity.LoginLog;
import org.haredot.utils.IpUtils;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 负责 生成 JWT 令牌
 */
public class JWTAuthenticationFilter extends OncePerRequestFilter {
    /**
     * 配置过滤器拦截的地址
     */
    private static final AntPathRequestMatcher TOKEN_MATCHER =
            new AntPathRequestMatcher("/api/token", "POST");

    private JwtToken jwtToken ;

    private AuthenticationManager authenticationManager ;

    private UserDetailsService userDetailsService ;

    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    /**
     * 记录登录日志
     */
    private RabbitTemplate rabbitTemplate ;


    public JWTAuthenticationFilter(JwtToken jwtToken, AuthenticationManager authenticationManager) {
        this.jwtToken = jwtToken ;
        this.authenticationManager = authenticationManager ;
        Assert.notNull(this.jwtToken, "jwtToken is null");
        Assert.notNull(this.authenticationManager, "authenticationManager is null");
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // 判断 是否是要处理的请求
        if (!TOKEN_MATCHER.matches(request)) {
            // 如果不是需要处理的请求、则继续向下执行
            filterChain.doFilter(request, response);
            return ;
        }


        // 解决跨域问题
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "*");
        response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");

        // 获取 refresh 参数
        String refresh = request.getParameter("refresh") ;
        // 如果 是 需要的请求， 获取 用户名 和 密码
        String username = request.getParameter("username") ;
        // 对获取的用户名 去空格处理
        username = username !=null ? username.trim() : "" ;
        String password = request.getParameter("password") ;
        password = password !=null ? password : "" ;

        if (StringUtils.hasLength(refresh) && (StringUtils.hasLength(username) || StringUtils.hasLength(password))) {

            Map<String, Object> status = Map.of("status", "20000",  "message", "refresh 和 username, password 不能同时传入");

            new MappingJackson2HttpMessageConverter()
                    .write(status , MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
            return ;
        }

        if (!StringUtils.hasLength(refresh)) { // 根据用户名 或 密码 进行认证

            // 构建一个 待认证的对象
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken
                    .unauthenticated(username, password);

            try {
                //记录 登录日志
                LoginLog log = LoginLongContextHolder.getLog();
                log.setUsername(username);
                // 获取浏览器和操作系统信息
                UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
                // 获取浏览器名称 和 版本号
                String browserName = userAgent.getBrowser().getName();

                log.setBrowserName(browserName);
                // 获取操作系统名
                String osName = userAgent.getOperatingSystem().getName();
                log.setOsName(osName);

                // 获取 IP地址
                String ipAddress = IpUtils.getIpAddress(request);
                log.setIp(ipAddress);
                // 获取真实地点
                log.setAddress(IpUtils.getRealAddress(ipAddress));

                // 使用 认证管理器 负责 认证 用户
                Authentication authentication = this.authenticationManager.authenticate(authRequest);
                // 获取 UserDetails
                UserDetails principal = (UserDetails) authentication.getPrincipal();

                successHandler(principal, response);

            } catch (AuthenticationException exception) {
                failHandler(exception, response) ;
            } catch (Exception e) {
                // 其他失败
                // 构建一个要返回的 JSON 对象
                Map<String, Object> status = Map.of("status", "30000", "message", e.getMessage());
                // 返回 一个 成功的 JSON ，并 包含 访问令牌
                new MappingJackson2HttpMessageConverter()
                        .write(status, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));

                LoginLog log = LoginLongContextHolder.getLog();
                log.setStatus(false);
                log.setMessage(e.getMessage());

            } finally {
                // 记录登录日志
                if (this.rabbitTemplate !=null) {
                    try {
                        rabbitTemplate.convertAndSend(RabbitConst.HAREDOT_LOG_EXCHANGE, "haredot.login.log", LoginLongContextHolder.getLog());
                    } catch (AmqpException e) {
                        e.printStackTrace();
                    }
                }
                // 从 ThreadLocal中移除，防止内存溢出
                LoginLongContextHolder.remove();
            }
        }else {
             // 验证 刷新 令牌是否 过期
            boolean validToken = jwtToken.isValidToken(refresh);

            if (!validToken) {
                Map<String, Object> status = Map.of("status", "20000", "message", "刷新令牌已过期或不正确");
                // 返回 一个 成功的 JSON ，并 包含 访问令牌
                new MappingJackson2HttpMessageConverter()
                        .write(status, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
                return ;
            }

            // 如果 token 认证成功 , 获取 Claims (获取用户名)
            Claims claims = jwtToken.parseJwtToClaims(refresh);
            // 获取用户名
            username = claims.getSubject() ;

            // 根据用户名、查询 数据
            try {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

                // 验证下 用户各种信息
                this.userDetailsChecker.check(userDetails);

                // 获取密码，并对密码进行校验
                boolean valid = jwtToken.isValidJti(username, claims.getExpiration(), userDetails.getPassword(), claims.getId()) ;

                // 如果 校验失败，说明 用户名， 或者 密码不正确
                if (!valid) {
                    Map<String, Object> status = Map.of("status", "20000", "message", "用户名或者密码不正确");
                    // 返回 一个 成功的 JSON ，并 包含 访问令牌
                    new MappingJackson2HttpMessageConverter()
                            .write(status, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
                    return ;
                }
                // 说明登录成功、负责 生成 JWT 访问令牌
                successHandler(userDetails, response);

            } catch (Exception e) {
                failHandler(e, response) ;
            }
        }
    }

    /**
     * 成功处理器
     * @param userDetails
     * @param response
     * @throws IOException
     */
    private void successHandler(UserDetails userDetails, HttpServletResponse response) throws IOException{
        String accessToken = jwtToken.generatorAccessToken(userDetails);
        // 负责 生成 JWT 刷新 令牌
        String refreshToken = jwtToken.generatorRefreshToken(userDetails);

        // 构建一个要返回的 JSON 对象
        Map<String, Object> status = Map.of("status", "10000", "accessToken", accessToken,
                "refreshToken", refreshToken, "type", "jwt");

        LoginLog log = LoginLongContextHolder.getLog();
        log.setStatus(true);

        new MappingJackson2HttpMessageConverter()
                .write(status, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
    }


    private void failHandler(Exception exception, HttpServletResponse response) throws IOException{
        String error = "";
        if (exception instanceof BadCredentialsException) {
            error = " 用户名或者密码不正确 ";
        } else if (exception instanceof DisabledException) {
            error = "账户被禁用、请联系管理员";
        } else if (exception instanceof AccountExpiredException) {
            error = "您的账户已被注销";
        } else if (exception instanceof LockedException) {
            error = "您的账户已被锁、请联系管理员解锁";
        } else if (exception instanceof CredentialsExpiredException) {
            error = "您的密码已过期、请更新您的密码";
        } else if (exception instanceof InternalAuthenticationServiceException) {
            error = exception.getMessage();
        } else {
            error = "认证失败";
        }

        LoginLog log = LoginLongContextHolder.getLog();
        log.setStatus(false);
        log.setMessage(error);
        // 构建一个要返回的 JSON 对象
        Map<String, Object> status = Map.of("status", "20000", "message", error);
        // 返回 一个 成功的 JSON ，并 包含 访问令牌
        new MappingJackson2HttpMessageConverter()
                .write(status, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));

    }
}
